import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
import java.net.SocketException;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Random;

/**
 * 
 * Authors - Oren Bracha, Rony Lahav and Yehuda Eterman
 * 
 * the Class handles the data receiving side of the application in TCP
 * sending request entry, receiving entries and updates
 */
public class TCPReceiverConnection implements Runnable {
	private Socket concreteSocket; // Will hold a reference for the specific socket
	private String senderName;

	private int inDebthForRcvWin = 0;
	boolean stopRequesting = false;
	
	String dbVersionOfSendingPeer;
	
	BufferedReader reader;
	PrintWriter pw;
	
	private RequestsManager reqManager = null;
	private RequestsManager picReqManager = null;
	
	long uniqueID;
	
	String connType = "tcp";

	// Constructor. Gets to the socket
	public TCPReceiverConnection(Socket socket, long uniqueID) throws SocketException
	{
		this.concreteSocket = socket;
		this.concreteSocket.setSoTimeout(proj350.TIMEOUT_IDLE_PERIOD);
		this.uniqueID = uniqueID;
	}
	
	public void run()
	{
		try{
			//create reader and writer for the TCP connection
			reader = new BufferedReader(new InputStreamReader(concreteSocket.getInputStream()));
			pw = new PrintWriter(concreteSocket.getOutputStream());
	
			// Wait for an update message until I get one. null will return If I didnt get one
			HashMap<String, String> parsedMessage = waitForSingleSpecificMessage(MessageCenter.TYPE_UPDATE);
			
			if (parsedMessage != null) {
				//receive message and set peer currently online
				senderName = parsedMessage.get(MessageCenter.VAR_PEERNAME);
				dbVersionOfSendingPeer = parsedMessage.get(MessageCenter.VAR_DBVERSION);
				DataBase.getInstance().setPeerOnline(senderName);
				DataBase.getInstance().setLastConnectionTime(senderName);
				
				//send appropriate request entry messages and receive entry messages
				boolean reqEntryStepSucc = HandleReqEntrySessionIfNeeded(parsedMessage);
				
				if (reqEntryStepSucc){
					if (reqManager != null){
						//take all the peers of which we received entries and update in DB  
						DBTuple[] dbTuples = reqManager.getEntries();
						String zeroDate = DateStringConverter.getZeroDateAsString();
						for (DBTuple tuple : dbTuples) {
							DBTuple tupleFromDB = DataBase.getInstance().findTuple(tuple.getPeerName());
							boolean isZeroDated = false;
							int isNewerPicVersion = 0;
							String peerName = null;
							//get data from DB to check for new pictures
							if (tupleFromDB != null){
								peerName = tupleFromDB.getPeerName();
								isZeroDated = tupleFromDB.getPeerCommitTime().equals(zeroDate);
								isNewerPicVersion = DateStringConverter.compare2Dates(tuple.getPicVersion(), tupleFromDB.getPicVersion());
							}
							else {
								peerName = tuple.getPeerName();
								isNewerPicVersion = 1;
								isZeroDated = true;
							}
							DataBase.getInstance().updateTupleIfRelevant(tuple);
							
							//if the peer has a new picture we add him to the list of people with new pictures 
							if ((isZeroDated && isNewerPicVersion>=0) || 
								(!isZeroDated && isNewerPicVersion>0)){
								DataBase.getInstance().addPeerThatHasNewPic(peerName, senderName);
							}
						}
					}
					
					//find all the peers that the updating peer can help me to acquire 
					String[] peersForReqPic = DataBase.getInstance().findPeersThatHavePeerInPicSet(senderName);
					
					if (peersForReqPic != null){	//begin request pictures sequence
						Random rand = new Random(System.currentTimeMillis());
						
						//try to get the picture 
						for (String peerForReqPic : peersForReqPic) {
							if (DataBase.getInstance().findProbabiltyForPictureUpdate(peerForReqPic)>rand.nextFloat()){
								DBTuple tuple = DataBase.getInstance().findTuple(peerForReqPic);
								//find the next segment we need for the picture
								String firstSegmentNumber = DataBase.getInstance().getNumberOfNextPicSegment(peerForReqPic);
								String dbVersion = tuple.getDbVersion();
								//allocate receive window as large as we can for the picture
								int rcvWinSpace = PictureHandler.allocateRcvWin(Integer.parseInt(tuple.getPicSize())-proj350.getMSS()/2*(Integer.parseInt(firstSegmentNumber)-1));
								inDebthForRcvWin += rcvWinSpace;
								if (rcvWinSpace !=0){
									rcvWinSpace*=2; // Data will arrive encoded in hexadecimal format - twice times bits
									int numOfFragments = rcvWinSpace/proj350.getMSS() + (rcvWinSpace%proj350.getMSS()==0?0:1);
									picReqManager = new RequestsManager(numOfFragments,Integer.parseInt(firstSegmentNumber));

									//find the last picture fragment that we will receive
									String lastSegmentNumber = Integer.toString((Integer.parseInt(firstSegmentNumber)+numOfFragments-1));
									//prepare a request picture message with range
									String requestPictureMsg = MessageCenter.getInstance().createMessage_RequestPicture_DB_RCV_Peer_Range(dbVersion, Integer.toString(rcvWinSpace), tuple.getPeerName(), firstSegmentNumber, lastSegmentNumber);
									//request a range of picture fragments and get the response
									boolean reqPicStepSucc = HandleReqPictureSession(requestPictureMsg,peerForReqPic,Integer.parseInt(firstSegmentNumber)-1);
									
									//check if we received the picture properly
									if (reqPicStepSucc && 
										Integer.parseInt(firstSegmentNumber) == Integer.parseInt(DataBase.getInstance().getNumberOfNextPicSegment(peerForReqPic))){
										String[] fragments = picReqManager.getPicture();
										//add the fragments that we received to the DB
										DataBase.getInstance().addSegmentsToPicture(tuple.getPeerName(), fragments);
									}
									
									//this is a check for event that the picture has changed  before we updated the
									// the pic into the DB
									if (!stopRequesting){
										//if we got the whole picture we insert it into the DB
										if (DataBase.getInstance().isPictureCompletelyReceived(tuple.getPeerName())){
											PictureHandler.combinePictureFromParts(tuple.getPeerName());
											
											String currTime = DateStringConverter.timeCurrentForMessages();
											tuple.setPeerCommitTime(currTime);
											DBTuple myTuple = proj350.getMyTuple();
											myTuple.setDbVersion(currTime);
											myTuple.setPeerCommitTime(currTime);
										}
									}
								}
							}
						}
					}
				}
			}
		}
		catch(IOException e) {
		}
		catch (Exception e2){
		}
		finally{
			try{
		
				if (inDebthForRcvWin > 0){
					proj350.getInstance().freeRcvWinSpace(inDebthForRcvWin);
				}
				else if (inDebthForRcvWin < 0) {
				}
				proj350.removeThreadFromLivingList(uniqueID);
				concreteSocket.close();
			}
			catch (Exception e){
			}
		}
	}

	/**
	 * the function sends request picture and receives in response a picture message
	 * @param reqPictureMessage
	 * @param peerForReqPic
	 * @param firstSegNum
	 * @return true if action was successful
	 * @throws IOException
	 */
	public boolean HandleReqPictureSession(String reqPictureMessage, String peerForReqPic, int firstSegNum) throws IOException{
		String requestPictureMessage = null;
		boolean stillValid = true;
		//send request picture
		if (requestPicturesFirstTime(reqPictureMessage,firstSegNum)){
			while (!picReqManager.isComplete() && stillValid && !stopRequesting){
				//find if there are missing fragments that disappeared
				String missingFragments = picReqManager.getMissingFragmentsList();
				HashMap<Integer, String> incIdToGlobIdMap = mapNewRequestsToSerials(missingFragments);
				
				//send request picture for the fragments we lost in the middle of transport
				requestPictureMessage = MessageCenter.getInstance().createMessage_RequestPicture_DB_RCV_Peer_List(dbVersionOfSendingPeer, Integer.toString(inDebthForRcvWin*2), peerForReqPic, missingFragments);
				
				pw.print(requestPictureMessage);
				pw.flush();
				Logger.getInstance().printInOutMsg(false, requestPictureMessage, proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), senderName, concreteSocket.getInetAddress().getHostAddress(), Integer.toString(concreteSocket.getPort()),connType);
				if (!waitForPictures(incIdToGlobIdMap))
				{
					stillValid = false;
				}
			}
		}
		else
		{
			stillValid = false;
		}
		
		return (stillValid && !stopRequesting);
	}
	
	/**
	 * a function to request when the peer first loads
	 * @param reqPictureMessage
	 * @param firstSegNum
	 * @return true if successful
	 * @throws IOException
	 */
	public boolean requestPicturesFirstTime(String reqPictureMessage, int firstSegNum) throws IOException{
		pw.write(reqPictureMessage);
		pw.flush();
		
		Logger.getInstance().printInOutMsg(false, reqPictureMessage, proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), senderName, concreteSocket.getInetAddress().getHostAddress(), Integer.toString(concreteSocket.getPort()),connType);
		
		boolean firstPictureAccepted = false;
		
		// Wait for the first ENTRY message to determine the number of entries I should expect
		HashMap<String, String> firstValidPictureMessage = null;
		do{
			// Wait for entry message 
			firstValidPictureMessage = waitForSingleSpecificMessage(MessageCenter.TYPE_PICTURE);

			//check if the picture we receive is obsolete
			if (firstValidPictureMessage != null){
				stopRequesting = checkExpiartionOfPicMsg(firstValidPictureMessage);
				
				if (!stopRequesting){
					firstPictureAccepted = true;
					
					//free the receive window by the data we received
					int pictureDatasize = firstValidPictureMessage.get(MessageCenter.VAR_DATA).length();
					proj350.getInstance().freeRcvWinSpace(pictureDatasize/2);
					inDebthForRcvWin -= pictureDatasize/2;
					int sn = Integer.parseInt(firstValidPictureMessage.get(MessageCenter.VAR_SN))+firstSegNum;
					//update picture fragments and handle 
					picReqManager.updateEntry(Integer.parseInt(firstValidPictureMessage.get(MessageCenter.VAR_SN)), Integer.toString(sn), null, firstValidPictureMessage.get(MessageCenter.VAR_DATA), true);	
				}
			}
		} while (firstValidPictureMessage != null && !picReqManager.isComplete() && !stopRequesting);
		
		return firstPictureAccepted;
	}
	
	/**
	 * sending request entry and receiving entries
	 * @param parsedMessage
	 * @return true if success
	 */
	public boolean HandleReqEntrySessionIfNeeded(HashMap<String, String> parsedMessage){
		String peerSenderDBVersion = parsedMessage.get(MessageCenter.VAR_DBVERSION);
		
		DBTuple senderTupleDetails = DataBase.getInstance().findTuple(senderName);
		
		boolean needEntries = false;
		boolean stillValid = true; 
		String requestEntryMessage = null;
		String dbVersionRequested = null;
		if (senderTupleDetails != null)
		{
			//check if need entries
			if (DateStringConverter.getMSFromStringMessageFormat(senderTupleDetails.getDbVersion()) <
				DateStringConverter.getMSFromStringMessageFormat(peerSenderDBVersion)) {
				requestEntryMessage = MessageCenter.getInstance().createMessage_RequestEntry_DB(senderTupleDetails.getDbVersion());
				dbVersionRequested = senderTupleDetails.getDbVersion();
				needEntries = true;
			}
		}
		else
		{
			//create message that requests all entries
			dbVersionRequested = "0";
			requestEntryMessage = MessageCenter.getInstance().createMessage_RequestEntry_DB("0");
			needEntries = true;
		}	
		//prepare requestEntry for every peer that needs it
		if (needEntries && requestEntriesFirstTime(requestEntryMessage)){
			while (!reqManager.isComplete() && stillValid){
				LinkedList<String[]> missingEntries = reqManager.getRangesList();
				for (String[] range : missingEntries) {
					//create message and send it
					requestEntryMessage = MessageCenter.getInstance().createMessage_RequestEntry_DB_Range(dbVersionRequested, range[0], range[1]);
					pw.print(requestEntryMessage);
					pw.flush();
					Logger.getInstance().printInOutMsg(false, requestEntryMessage, proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), senderName, concreteSocket.getInetAddress().getHostAddress(), Integer.toString(concreteSocket.getPort()),connType);
				}
				if (!waitForEntries())
				{
					stillValid = false;
				}
			}
		}
		else
		{
			if (needEntries){
				stillValid = false;
			}
		}
		
		return stillValid;
	}
	
	/**
	 * 
	 * This method perform the requestentry/entry for the first time to determine how many entries
	 * I need to get from the sender
	 */
	public boolean requestEntriesFirstTime(String reqEntryMessage){
		// Transmit the requestentry message and wait for entriesw
		pw.print(reqEntryMessage);
		pw.flush();
		Logger.getInstance().printInOutMsg(false, reqEntryMessage, proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), senderName, concreteSocket.getInetAddress().getHostAddress(), Integer.toString(concreteSocket.getPort()),connType);
		
		boolean firstEntryAccepted = false;
		
		// Wait for the first ENTRY message to determine the number of entries I should expect
		HashMap<String, String> firstValidEntryMessage = null;
		do{
			// Wait for entry message 
			firstValidEntryMessage = waitForSingleSpecificMessage(MessageCenter.TYPE_ENTRY);
			
			if (firstValidEntryMessage != null){
				firstEntryAccepted = true;
				
				// Get the number of expected entries
				int numExpectedEntries = Integer.parseInt(firstValidEntryMessage.get(MessageCenter.VAR_N));
				
				DBTuple tuple = createDBTupleFromEntryMessage(firstValidEntryMessage);
				
				// Fill the request manager with the accepted entries
				if (reqManager == null){
					reqManager = new RequestsManager(numExpectedEntries,0);
				}
				
				reqManager.updateEntry(Integer.parseInt(firstValidEntryMessage.get(MessageCenter.VAR_SN)), firstValidEntryMessage.get(MessageCenter.VAR_PEERNAME), tuple, null, true);	
			}
		} while (firstValidEntryMessage != null && !reqManager.isComplete());
		
		
		return firstEntryAccepted;
	}
	
	/**
	 * 
	 * This method gets a message type and wait to read from the socket until a message of this
	 * type is accepted (or a timeout reached)
	 */
	public HashMap<String, String> waitForSingleSpecificMessage(String messageType){
		HashMap<String, String> parsedMessage = null;
		boolean isValidMessage = false;
		String messageRead = null;
		do {
			try{
				messageRead = readMessage();
			}
			catch(IOException e){
				messageRead = null;
			}
			
			// Parse the message
			parsedMessage= MessageCenter.getInstance().parseValidMessage(messageRead, senderName, concreteSocket.getInetAddress().getHostAddress(), Integer.toString(concreteSocket.getPort()), proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()),true);
			if (parsedMessage != null){
				// Check if the accepted message is the type I want
				isValidMessage = (parsedMessage.get(MessageCenter.TYPE_MESSAGE).compareTo(messageType) == 0);
				if (!isValidMessage){
					Logger.getInstance().printErrorWrongFormat(messageType, messageRead, senderName, concreteSocket.getInetAddress().getHostAddress(), Integer.toString(concreteSocket.getPort()), proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()),connType);
				}
				else
				{
					Logger.getInstance().printInOutMsg(true, messageRead, senderName, concreteSocket.getInetAddress().getHostAddress(), Integer.toString(concreteSocket.getPort()),proj350.getMyPeerName(), proj350.getMyIp(), Integer.toString(concreteSocket.getLocalPort()), connType);
				}
			}
		// Continue to read until you get the type of message you want or socket was closed 
		}while(!isValidMessage && messageRead != null);
		return parsedMessage;		
	}
	
	/**
	 * 
	 * this method wait for entries message and handles them
	 */
	public boolean waitForEntries() {
		HashMap<String, String> parsedMessage = null;
		
		boolean atLeastOneAccepted = false;
		
		do {
			// Wait for entry message
			parsedMessage = waitForSingleSpecificMessage(MessageCenter.TYPE_ENTRY);

			//update entry if it is valid
			if (parsedMessage!=null){
				atLeastOneAccepted = true;
				DBTuple dbTuple = createDBTupleFromEntryMessage(parsedMessage);
				reqManager.updateEntry(Integer.parseInt(parsedMessage.get(MessageCenter.VAR_SN)), dbTuple.getPeerName(), dbTuple, null, false);
			}		
		} while (parsedMessage!=null);
		
		return atLeastOneAccepted;
	}
	
	/**
	 * receiving entry pictures
	 * @param map
	 * @return
	 */
	public boolean waitForPictures(HashMap<Integer, String> map) {
		HashMap<String, String> parsedMessage = null;
		
		boolean atLeastOneAccepted = false;
		
		do {
			// Wait for entry message
			parsedMessage = waitForSingleSpecificMessage(MessageCenter.TYPE_PICTURE);
				
			if (parsedMessage!=null){
				stopRequesting = checkExpiartionOfPicMsg(parsedMessage);
				if (!stopRequesting){
					atLeastOneAccepted = true;
					int dataSize = parsedMessage.get(MessageCenter.VAR_DATA).length();
					proj350.getInstance().freeRcvWinSpace(dataSize/2);
					inDebthForRcvWin -= dataSize/2;
					picReqManager.updateEntry(Integer.parseInt(parsedMessage.get(MessageCenter.VAR_SN)), map.get(Integer.parseInt(parsedMessage.get(MessageCenter.VAR_SN))), null, parsedMessage.get(MessageCenter.VAR_DATA), false);
				}
			}		
		} while (parsedMessage!=null && !stopRequesting);
		
		return (atLeastOneAccepted && !stopRequesting);
	}

	/**
	 * check if the picture that we are receiving has been changed yet 
	 * @param parsedMessage
	 * @return
	 */
	private boolean checkExpiartionOfPicMsg(HashMap<String, String> parsedMessage){
		String peerName = parsedMessage.get(MessageCenter.VAR_PEERNAME);
		String acceptedPicVer = parsedMessage.get(MessageCenter.VAR_PICVERSION);
		String currPicVer = DataBase.getInstance().findTuple(peerName).getPicVersion();
		
		return !(DateStringConverter.compare2Dates(acceptedPicVer, currPicVer) == 0);
	}
	
	/**
	 * 
	 * this method creates a DBTuple entity from an entry message and returns it
	 */
	public DBTuple createDBTupleFromEntryMessage(HashMap<String, String> entryMessage){
		DBTuple tuple = new DBTuple();
		tuple.setDbVersion(entryMessage.get(MessageCenter.VAR_DBVERSION));
		tuple.setPeerVersion(entryMessage.get(MessageCenter.VAR_PEERVERSION));
		tuple.setPeerName(entryMessage.get(MessageCenter.VAR_PEERNAME));
		tuple.setIp(entryMessage.get(MessageCenter.VAR_IP));
		tuple.setTcpPort(entryMessage.get(MessageCenter.VAR_TCP));
		tuple.setUdpPort(entryMessage.get(MessageCenter.VAR_UDP));
		tuple.setPhoneNum(entryMessage.get(MessageCenter.VAR_PHONE));
		tuple.setEmailAddress(entryMessage.get(MessageCenter.VAR_EMAIL));
		tuple.setLastConnectionTime(entryMessage.get(MessageCenter.VAR_LASTCONTIME));
		tuple.setStatus(entryMessage.get(MessageCenter.VAR_STATUS));
		tuple.setPicSize(entryMessage.get(MessageCenter.VAR_PICSIZE));
		tuple.setPicVersion(entryMessage.get(MessageCenter.VAR_PICVERSION));
		
		return tuple;
	}
	
	/**
	 * 
	 * This method read a single message from the socket. The end of socket is determined by the
	 * ">" character. If the message doesn't contain ">" character (illegal) then we stop reading
	 * after some number of characters
	 */
	String readMessage() throws IOException{
		char terminatingChar = '>';
		String message = "";
		char charRead = 'a';
		int intRead = 0;
		int mss = proj350.getMSS();
		for (int i=0; (i<mss*2 && charRead!=terminatingChar && intRead!=-1); ++i) {
			intRead = reader.read();
			if (intRead != -1){
				charRead = (char)intRead;
				message += charRead;
			}
		}
		if (message.compareTo("")==0){
			message = null;
		}
		
		return message;
	}
	
	/**
	 * This method maps fragments numbers to serial number of them in message
	 */
	private HashMap<Integer, String> mapNewRequestsToSerials(String newListRequest){
		String[] fragments = newListRequest.split(",");
		HashMap<Integer,String> serialToFragmentNumMap = new HashMap<Integer,String>();
		
		for (int i=1; i<=fragments.length; ++i){
			serialToFragmentNumMap.put(i, fragments[i-1]);
		}
		
		return serialToFragmentNumMap;
	}
	
	public long getUniqueID(){
		return uniqueID;
	}
}
